#ifdef WIN32
    // Why?
    #define _WIN32_WINNT 0x0502
    // The standard Windows includes.
    #ifndef WIN32_LEAN_AND_MEAN
        #define WIN32_LEAN_AND_MEAN
    #endif // WIN32_LEAN_AND_MEAN
    #ifndef NOMINMAX
        #define NOMINMAX
    #endif // NOMINMAX
    #include <Windows.h>
    #include <wchar.h>
    #ifdef SLIC3R_GUI
    extern "C"
    {
        // Let the NVIDIA and AMD know we want to use their graphics card
        // on a dual graphics card system.
        __declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001;
        __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
    }
    #endif /* SLIC3R_GUI */
#endif /* WIN32 */

#include <cstdio>
#include <string>
#include <cstring>
#include <iostream>
#include <math.h>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/filesystem.hpp>
#include <boost/nowide/args.hpp>
#include <boost/nowide/cenv.hpp>
#include <boost/nowide/iostream.hpp>
#include <boost/nowide/integration/filesystem.hpp>
#include <boost/dll/runtime_symbol_info.hpp>

#include "unix/fhs.hpp"  // Generated by CMake from ../platform/unix/fhs.hpp.in

#include "libslic3r/libslic3r.h"
#if ENABLE_GL_CORE_PROFILE
#include <boost/algorithm/string/split.hpp>
#endif // ENABLE_GL_CORE_PROFILE
#include "libslic3r/Config.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/GCode/PostProcessor.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/CutUtils.hpp"
#include "libslic3r/ModelArrange.hpp"
#include "libslic3r/Platform.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/SLAPrint.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/Format/AMF.hpp"
#include "libslic3r/Format/3mf.hpp"
#include "libslic3r/Format/STL.hpp"
#include "libslic3r/Format/OBJ.hpp"
#include "libslic3r/Format/SL1.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/Thread.hpp"
#include "libslic3r/BlacklistedLibraryCheck.hpp"

#include "PrusaSlicer.hpp"

#ifdef SLIC3R_GUI
    #include "slic3r/GUI/GUI_Init.hpp"
#endif /* SLIC3R_GUI */

using namespace Slic3r;

static PrinterTechnology get_printer_technology(const DynamicConfig &config)
{
    const ConfigOptionEnum<PrinterTechnology> *opt = config.option<ConfigOptionEnum<PrinterTechnology>>("printer_technology");
    return (opt == nullptr) ? ptUnknown : opt->value;
}

int CLI::run(int argc, char **argv)
{
    // Mark the main thread for the debugger and for runtime checks.
    set_current_thread_name("slic3r_main");
    // Save the thread ID of the main thread.
    save_main_thread_id();

#ifdef __WXGTK__
    // On Linux, wxGTK has no support for Wayland, and the app crashes on
    // startup if gtk3 is used. This env var has to be set explicitly to
    // instruct the window manager to fall back to X server mode.
    ::setenv("GDK_BACKEND", "x11", /* replace */ true);
#endif

	// Switch boost::filesystem to utf8.
    try {
        boost::nowide::nowide_filesystem();
    } catch (const std::runtime_error& ex) {
        std::string caption = std::string(SLIC3R_APP_NAME) + " Error";
        std::string text = std::string("An error occured while setting up locale.\n") + (
#if !defined(_WIN32) && !defined(__APPLE__)
        	// likely some linux system
        	"You may need to reconfigure the missing locales, likely by running the \"locale-gen\" and \"dpkg-reconfigure locales\" commands.\n"
#endif
        	SLIC3R_APP_NAME " will now terminate.\n\n") + ex.what();
    #if defined(_WIN32) && defined(SLIC3R_GUI)
        if (m_actions.empty())
        	// Empty actions means Slicer is executed in the GUI mode. Show a GUI message.
            MessageBoxA(NULL, text.c_str(), caption.c_str(), MB_OK | MB_ICONERROR);
    #endif
        boost::nowide::cerr << text.c_str() << std::endl;
        return 1;
    }

	if (! this->setup(argc, argv))
		return 1;

    m_extra_config.apply(m_config, true);
    m_extra_config.normalize_fdm();
    
    PrinterTechnology printer_technology = get_printer_technology(m_config);

    bool							start_gui			= m_actions.empty() &&
        // cutting transformations are setting an "export" action.
        std::find(m_transforms.begin(), m_transforms.end(), "cut") == m_transforms.end() &&
        std::find(m_transforms.begin(), m_transforms.end(), "cut_x") == m_transforms.end() &&
        std::find(m_transforms.begin(), m_transforms.end(), "cut_y") == m_transforms.end();
    bool                            start_downloader = false;
    bool                            delete_after_load = false;
    std::string                     download_url;
    bool 							start_as_gcodeviewer =
#ifdef _WIN32
            false;
#else
            // On Unix systems, the prusa-slicer binary may be symlinked to give the application a different meaning.
            boost::algorithm::iends_with(boost::filesystem::path(argv[0]).filename().string(), "gcodeviewer");
#endif // _WIN32

    const std::vector<std::string>              &load_configs		      = m_config.option<ConfigOptionStrings>("load", true)->values;
    const ForwardCompatibilitySubstitutionRule   config_substitution_rule = m_config.option<ConfigOptionEnum<ForwardCompatibilitySubstitutionRule>>("config_compatibility", true)->value;

    // load config files supplied via --load
    for (auto const &file : load_configs) {
        if (! boost::filesystem::exists(file)) {
            if (m_config.opt_bool("ignore_nonexistent_config")) {
                continue;
            } else {
                boost::nowide::cerr << "No such file: " << file << std::endl;
                return 1;
            }
        }
        DynamicPrintConfig  config;
        ConfigSubstitutions config_substitutions;
        try {
            config_substitutions = config.load(file, config_substitution_rule);
        } catch (std::exception &ex) {
            boost::nowide::cerr << "Error while reading config file \"" << file << "\": " << ex.what() << std::endl;
            return 1;
        }
        if (! config_substitutions.empty()) {
            boost::nowide::cout << "The following configuration values were substituted when loading \" << file << \":\n";
            for (const ConfigSubstitution &subst : config_substitutions)
                boost::nowide::cout << "\tkey = \"" << subst.opt_def->opt_key << "\"\t loaded = \"" << subst.old_value << "\tsubstituted = \"" << subst.new_value->serialize() << "\"\n";
        }
        config.normalize_fdm();
        PrinterTechnology other_printer_technology = get_printer_technology(config);
        if (printer_technology == ptUnknown) {
            printer_technology = other_printer_technology;
        } else if (printer_technology != other_printer_technology && other_printer_technology != ptUnknown) {
            boost::nowide::cerr << "Mixing configurations for FFF and SLA technologies" << std::endl;
            return 1;
        }
        m_print_config.apply(config);
    }

#ifdef SLIC3R_GUI
#if ENABLE_GL_CORE_PROFILE
    std::pair<int, int> opengl_version = { 0, 0 };
    bool                opengl_debug = false;
    bool                opengl_compatibility_profile = false;

    // search for special keys into command line parameters
    auto it = std::find(m_actions.begin(), m_actions.end(), "gcodeviewer");
    if (it != m_actions.end()) {
        start_gui = true;
        start_as_gcodeviewer = true;
        m_actions.erase(it);
    }

    it = std::find(m_actions.begin(), m_actions.end(), "opengl-version");
    if (it != m_actions.end()) {
        const Semver opengl_minimum = Semver(3,2,0);
        const std::string opengl_version_str = m_config.opt_string("opengl-version");
        boost::optional<Semver> semver = Semver::parse(opengl_version_str);
        if (semver.has_value() && (*semver) >= opengl_minimum ) {
            opengl_version.first = semver->maj();
            opengl_version.second = semver->min();
            if (std::find(Slic3r::GUI::OpenGLVersions::core.begin(), Slic3r::GUI::OpenGLVersions::core.end(), std::make_pair(opengl_version.first, opengl_version.second)) == Slic3r::GUI::OpenGLVersions::core.end()) {
                opengl_version = { 0, 0 };
                boost::nowide::cerr << "Required OpenGL version " << opengl_version_str << " not recognized.\n Option 'opengl-version' ignored." << std::endl;
            }
        } else
            boost::nowide::cerr << "Required OpenGL version " << opengl_version_str << " is invalid. Must be greater than or equal to " <<
                opengl_minimum.to_string() << "\n Option 'opengl-version' ignored." << std::endl;
        start_gui = true;
        m_actions.erase(it);
    }

    it = std::find(m_actions.begin(), m_actions.end(), "opengl-compatibility");
    if (it != m_actions.end()) {
        start_gui = true;
        opengl_compatibility_profile = true;
        // reset version as compatibility profile always take the highest version
        // supported by the graphic card
        opengl_version = std::make_pair(0, 0);
        m_actions.erase(it);
    }

    it = std::find(m_actions.begin(), m_actions.end(), "opengl-debug");
    if (it != m_actions.end()) {
        start_gui = true;
        opengl_debug = true;
        m_actions.erase(it);
    }
#else
    // are we starting as gcodeviewer ?
    for (auto it = m_actions.begin(); it != m_actions.end(); ++it) {
        if (*it == "gcodeviewer") {
            start_gui = true;
            start_as_gcodeviewer = true;
            m_actions.erase(it);
            break;
        }
    }
#endif // ENABLE_GL_CORE_PROFILE
#else // SLIC3R_GUI
    // If there is no GUI, we shall ignore the parameters. Remove them from the list.
    for (const std::string& s : { "opengl-version", "opengl-compatibility", "opengl-debug", "gcodeviewer" }) {
        auto it = std::find(m_actions.cbegin(), m_actions.cend(), s);
        if (it != m_actions.end()) {
            boost::nowide::cerr << "Parameter '" << s << "' is ignored, this PrusaSlicer build is CLI only." << std::endl;
            m_actions.erase(it);
        }
    }
#endif


    // Read input file(s) if any.
    for (const std::string& file : m_input_files)
        if (is_gcode_file(file) && boost::filesystem::exists(file)) {
            start_as_gcodeviewer = true;
            break;
        }
    if (!start_as_gcodeviewer) {
        for (const std::string& file : m_input_files) {
            if (boost::starts_with(file, "prusaslicer://")) {
                start_downloader = true;
                download_url = file;
                continue;
            }
            if (!boost::filesystem::exists(file)) {
                boost::nowide::cerr << "No such file: " << file << std::endl;
                exit(1);
            }
            Model model;
            try {
                // When loading an AMF or 3MF, config is imported as well, including the printer technology.
                DynamicPrintConfig config;
                ConfigSubstitutionContext config_substitutions(config_substitution_rule);
                //FIXME should we check the version here? // | Model::LoadAttribute::CheckVersion ?
                model = Model::read_from_file(file, &config, &config_substitutions, Model::LoadAttribute::AddDefaultInstances);
                PrinterTechnology other_printer_technology = get_printer_technology(config);
                if (printer_technology == ptUnknown) {
                    printer_technology = other_printer_technology;
                }
                else if (printer_technology != other_printer_technology && other_printer_technology != ptUnknown) {
                    boost::nowide::cerr << "Mixing configurations for FFF and SLA technologies" << std::endl;
                    return 1;
                }
                if (! config_substitutions.substitutions.empty()) {
                    boost::nowide::cout << "The following configuration values were substituted when loading \" << file << \":\n";
                    for (const ConfigSubstitution& subst : config_substitutions.substitutions)
                        boost::nowide::cout << "\tkey = \"" << subst.opt_def->opt_key << "\"\t loaded = \"" << subst.old_value << "\tsubstituted = \"" << subst.new_value->serialize() << "\"\n";
                }
                // config is applied to m_print_config before the current m_config values.
                config += std::move(m_print_config);
                m_print_config = std::move(config);
            }
            catch (std::exception& e) {
                boost::nowide::cerr << file << ": " << e.what() << std::endl;
                return 1;
            }
            if (model.objects.empty()) {
                boost::nowide::cerr << "Error: file is empty: " << file << std::endl;
                continue;
            }
            m_models.push_back(model);
        }
    }

    if (!start_gui) {
        const auto* post_process = m_print_config.opt<ConfigOptionStrings>("post_process");
        if (post_process != nullptr && !post_process->values.empty()) {
            boost::nowide::cout << "\nA post-processing script has been detected in the config data:\n\n";
            for (const auto& s : post_process->values) {
                boost::nowide::cout << "> " << s << "\n";
            }
            boost::nowide::cout << "\nContinue(Y/N) ? ";
            char in;
            boost::nowide::cin >> in;
            if (in != 'Y' && in != 'y')
                return 0;
        }
    }

    // Apply command line options to a more specific DynamicPrintConfig which provides normalize()
    // (command line options override --load files)
    m_print_config.apply(m_extra_config, true);
    // Normalizing after importing the 3MFs / AMFs
    m_print_config.normalize_fdm();

    if (printer_technology == ptUnknown)
        printer_technology = std::find(m_actions.begin(), m_actions.end(), "export_sla") == m_actions.end() ? ptFFF : ptSLA;
    m_print_config.option<ConfigOptionEnum<PrinterTechnology>>("printer_technology", true)->value = printer_technology;

    // Initialize full print configs for both the FFF and SLA technologies.
    FullPrintConfig    fff_print_config;
    SLAFullPrintConfig sla_print_config;
    
    // Synchronize the default parameters and the ones received on the command line.
    if (printer_technology == ptFFF) {
        fff_print_config.apply(m_print_config, true);
        m_print_config.apply(fff_print_config, true);
    } else {
        assert(printer_technology == ptSLA);
        sla_print_config.output_filename_format.value = "[input_filename_base].sl1";
        
        // The default bed shape should reflect the default display parameters
        // and not the fff defaults.
        double w = sla_print_config.display_width.getFloat();
        double h = sla_print_config.display_height.getFloat();
        sla_print_config.bed_shape.values = { Vec2d(0, 0), Vec2d(w, 0), Vec2d(w, h), Vec2d(0, h) };
        
        sla_print_config.apply(m_print_config, true);
        m_print_config.apply(sla_print_config, true);
    }
    
    {
        std::string validity = m_print_config.validate();
        if (! validity.empty()) {
            boost::nowide::cerr << "Error: The composite configation is not valid: " << validity << std::endl;
            return 1;
        }
    }
    
    // Loop through transform options.
    bool user_center_specified = false;
    arr2::ArrangeBed bed = arr2::to_arrange_bed(get_bed_shape(m_print_config));
    arr2::ArrangeSettings arrange_cfg;
    arrange_cfg.set_distance_from_objects(min_object_distance(m_print_config));

    for (auto const &opt_key : m_transforms) {
        if (opt_key == "merge") {
            Model m;
            for (auto &model : m_models)
                for (ModelObject *o : model.objects)
                    m.add_object(*o);
            // Rearrange instances unless --dont-arrange is supplied
            if (! m_config.opt_bool("dont_arrange")) {
                m.add_default_instances();
                if (this->has_print_action())
                    arrange_objects(m, bed, arrange_cfg);
                else
                    arrange_objects(m, arr2::InfiniteBed{}, arrange_cfg);
            }
            m_models.clear();
            m_models.emplace_back(std::move(m));
        } else if (opt_key == "duplicate") {
            for (auto &model : m_models) {
                const bool all_objects_have_instances = std::none_of(
                    model.objects.begin(), model.objects.end(),
                    [](ModelObject* o){ return o->instances.empty(); }
                );
                
                int dups = m_config.opt_int("duplicate");
                if (!all_objects_have_instances) model.add_default_instances();
                
                try {
                    if (dups > 1) {
                        // if all input objects have defined position(s) apply duplication to the whole model
                        duplicate(model, size_t(dups), bed, arrange_cfg);
                    } else {
                        arrange_objects(model, bed, arrange_cfg);
                    }
                } catch (std::exception &ex) {
                    boost::nowide::cerr << "error: " << ex.what() << std::endl;
                    return 1;
                }
            }
        } else if (opt_key == "duplicate_grid") {
            std::vector<int> &ints = m_config.option<ConfigOptionInts>("duplicate_grid")->values;
            const int x = ints.size() > 0 ? ints.at(0) : 1;
            const int y = ints.size() > 1 ? ints.at(1) : 1;
            const double distance = fff_print_config.duplicate_distance.value;
            for (auto &model : m_models)
                model.duplicate_objects_grid(x, y, (distance > 0) ? distance : 6);  // TODO: this is not the right place for setting a default
        } else if (opt_key == "center") {
        	user_center_specified = true;
            for (auto &model : m_models) {
                model.add_default_instances();
                // this affects instances:
                model.center_instances_around_point(m_config.option<ConfigOptionPoint>("center")->value);
                // this affects volumes:
                //FIXME Vojtech: Who knows why the complete model should be aligned with Z as a single rigid body?
                //model.align_to_ground();
                BoundingBoxf3 bbox;
                for (ModelObject *model_object : model.objects)
                    // We are interested into the Z span only, therefore it is sufficient to measure the bounding box of the 1st instance only.
                    bbox.merge(model_object->instance_bounding_box(0, false));
                for (ModelObject *model_object : model.objects)
                    for (ModelInstance *model_instance : model_object->instances)
                        model_instance->set_offset(Z, model_instance->get_offset(Z) - bbox.min.z());
            }
        } else if (opt_key == "align_xy") {
            const Vec2d &p = m_config.option<ConfigOptionPoint>("align_xy")->value;
            for (auto &model : m_models) {
                BoundingBoxf3 bb = model.bounding_box_exact();
                // this affects volumes:
                model.translate(-(bb.min.x() - p.x()), -(bb.min.y() - p.y()), -bb.min.z());
            }
        } else if (opt_key == "dont_arrange") {
            // do nothing - this option alters other transform options
        } else if (opt_key == "ensure_on_bed") {
            // do nothing, the value is used later
        } else if (opt_key == "rotate") {
            for (auto &model : m_models)
                for (auto &o : model.objects)
                    // this affects volumes:
                    o->rotate(Geometry::deg2rad(m_config.opt_float(opt_key)), Z);
        } else if (opt_key == "rotate_x") {
            for (auto &model : m_models)
                for (auto &o : model.objects)
                    // this affects volumes:
                    o->rotate(Geometry::deg2rad(m_config.opt_float(opt_key)), X);
        } else if (opt_key == "rotate_y") {
            for (auto &model : m_models)
                for (auto &o : model.objects)
                    // this affects volumes:
                    o->rotate(Geometry::deg2rad(m_config.opt_float(opt_key)), Y);
        } else if (opt_key == "scale") {
            for (auto &model : m_models)
                for (auto &o : model.objects)
                    // this affects volumes:
                    o->scale(m_config.get_abs_value(opt_key, 1));
        } else if (opt_key == "scale_to_fit") {
            const Vec3d &opt = m_config.opt<ConfigOptionPoint3>(opt_key)->value;
            if (opt.x() <= 0 || opt.y() <= 0 || opt.z() <= 0) {
                boost::nowide::cerr << "--scale-to-fit requires a positive volume" << std::endl;
                return 1;
            }
            for (auto &model : m_models)
                for (auto &o : model.objects)
                    // this affects volumes:
                    o->scale_to_fit(opt);
        } else if (opt_key == "cut" || opt_key == "cut_x" || opt_key == "cut_y") {
            std::vector<Model> new_models;
            for (auto &model : m_models) {
                model.translate(0, 0, -model.bounding_box_exact().min.z());  // align to z = 0
                size_t num_objects = model.objects.size();
                for (size_t i = 0; i < num_objects; ++ i) {

#if 0
                    if (opt_key == "cut_x") {
                        o->cut(X, m_config.opt_float("cut_x"), &out);
                    } else if (opt_key == "cut_y") {
                        o->cut(Y, m_config.opt_float("cut_y"), &out);
                    } else if (opt_key == "cut") {
                        o->cut(Z, m_config.opt_float("cut"), &out);
                    }
#else
//                    model.objects.front()->cut(0, m_config.opt_float("cut"), ModelObjectCutAttribute::KeepLower | ModelObjectCutAttribute::KeepUpper | ModelObjectCutAttribute::FlipLower);
                    Cut cut(model.objects.front(), 0, Geometry::translation_transform(m_config.opt_float("cut") * Vec3d::UnitZ()),
                                               ModelObjectCutAttribute::KeepLower | ModelObjectCutAttribute::KeepUpper | ModelObjectCutAttribute::PlaceOnCutUpper);
                    auto cut_objects = cut.perform_with_plane();
                    for (ModelObject* obj : cut_objects)
                        model.add_object(*obj);
#endif
                    model.delete_object(size_t(0));
                }
            }

            // TODO: copy less stuff around using pointers
            m_models = new_models;

            if (m_actions.empty())
                m_actions.push_back("export_stl");
        }
#if 0
        else if (opt_key == "cut_grid") {
            std::vector<Model> new_models;
            for (auto &model : m_models) {
                TriangleMesh mesh = model.mesh();
                mesh.repair();

                std::vector<TriangleMesh> meshes = mesh.cut_by_grid(m_config.option<ConfigOptionPoint>("cut_grid")->value);
                size_t i = 0;
                for (TriangleMesh* m : meshes) {
                    Model out;
                    auto o = out.add_object();
                    o->add_volume(*m);
                    o->input_file += "_" + std::to_string(i++);
                    delete m;
                }
            }

            // TODO: copy less stuff around using pointers
            m_models = new_models;

            if (m_actions.empty())
                m_actions.push_back("export_stl");
        }
#endif
        else if (opt_key == "split") {
            for (Model &model : m_models) {
                size_t num_objects = model.objects.size();
                for (size_t i = 0; i < num_objects; ++ i) {
                    ModelObjectPtrs new_objects;
                    model.objects.front()->split(&new_objects);
                    model.delete_object(size_t(0));
                }
            }
        } else if (opt_key == "repair") {
            // Models are repaired by default.
            //for (auto &model : m_models)
            //    model.repair();

        } else if (opt_key == "delete-after-load") {
            delete_after_load = true;
        } else {
            boost::nowide::cerr << "error: option not implemented yet: " << opt_key << std::endl;
            return 1;
        }
    }

    // All transforms have been dealt with. Now ensure that the objects are on bed.
    // (Unless the user said otherwise.)
    if (m_config.opt_bool("ensure_on_bed"))
        for (auto &model : m_models)
            for (auto &o : model.objects)
                o->ensure_on_bed();

    // loop through action options
    for (auto const &opt_key : m_actions) {
        if (opt_key == "help") {
            this->print_help();
        } else if (opt_key == "help_fff") {
            this->print_help(true, ptFFF);
        } else if (opt_key == "help_sla") {
            this->print_help(true, ptSLA);
        } else if (opt_key == "save") {
            //FIXME check for mixing the FFF / SLA parameters.
            // or better save fff_print_config vs. sla_print_config
            m_print_config.save(m_config.opt_string("save"));
        } else if (opt_key == "info") {
            // --info works on unrepaired model
            for (Model &model : m_models) {
                model.add_default_instances();
                model.print_info();
            }
        } else if (opt_key == "export_stl") {
            for (auto &model : m_models)
                model.add_default_instances();
            if (! this->export_models(IO::STL))
                return 1;
        } else if (opt_key == "export_obj") {
            for (auto &model : m_models)
                model.add_default_instances();
            if (! this->export_models(IO::OBJ))
                return 1;
        } else if (opt_key == "export_amf") {
            if (! this->export_models(IO::AMF))
                return 1;
        } else if (opt_key == "export_3mf") {
            if (! this->export_models(IO::TMF))
                return 1;
        } else if (opt_key == "export_gcode" || opt_key == "export_sla" || opt_key == "slice") {
            if (opt_key == "export_gcode" && printer_technology == ptSLA) {
                boost::nowide::cerr << "error: cannot export G-code for an FFF configuration" << std::endl;
                return 1;
            } else if (opt_key == "export_sla" && printer_technology == ptFFF) {
                boost::nowide::cerr << "error: cannot export SLA slices for a SLA configuration" << std::endl;
                return 1;
            }
            // Make a copy of the model if the current action is not the last action, as the model may be
            // modified by the centering and such.
            Model model_copy;
            bool  make_copy = &opt_key != &m_actions.back();
            for (Model &model_in : m_models) {
                if (make_copy)
                    model_copy = model_in;
                Model &model = make_copy ? model_copy : model_in;
                // If all objects have defined instances, their relative positions will be
                // honored when printing (they will be only centered, unless --dont-arrange
                // is supplied); if any object has no instances, it will get a default one
                // and all instances will be rearranged (unless --dont-arrange is supplied).
                std::string outfile = m_config.opt_string("output");
                Print       fff_print;
                SLAPrint    sla_print;
                sla_print.set_status_callback(
                            [](const PrintBase::SlicingStatus& s)
                {
                    if(s.percent >= 0) // FIXME: is this sufficient?
                        printf("%3d%s %s\n", s.percent, "% =>", s.text.c_str());
                });

                PrintBase  *print = (printer_technology == ptFFF) ? static_cast<PrintBase*>(&fff_print) : static_cast<PrintBase*>(&sla_print);
                if (! m_config.opt_bool("dont_arrange")) {
                    if (user_center_specified) {
                        Vec2d c = m_config.option<ConfigOptionPoint>("center")->value;
                        arrange_objects(model, arr2::InfiniteBed{scaled(c)}, arrange_cfg);
                    } else
                        arrange_objects(model, bed, arrange_cfg);
                }
                if (printer_technology == ptFFF) {
                    for (auto* mo : model.objects)
                        fff_print.auto_assign_extruders(mo);
                }
                print->apply(model, m_print_config);
                std::string err = print->validate();
                if (! err.empty()) {
                    boost::nowide::cerr << err << std::endl;
                    return 1;
                }
                if (print->empty())
                    boost::nowide::cout << "Nothing to print for " << outfile << " . Either the print is empty or no object is fully inside the print volume." << std::endl;
                else
                    try {
                        std::string outfile_final;
                        print->process();
                        if (printer_technology == ptFFF) {
                            // The outfile is processed by a PlaceholderParser.
                            outfile = fff_print.export_gcode(outfile, nullptr, nullptr);
                            outfile_final = fff_print.print_statistics().finalize_output_path(outfile);
                        } else {
                            outfile = sla_print.output_filepath(outfile);
                            // We need to finalize the filename beforehand because the export function sets the filename inside the zip metadata
                            outfile_final = sla_print.print_statistics().finalize_output_path(outfile);
                            sla_print.export_print(outfile_final);
                        }
                        if (outfile != outfile_final) {
                            if (Slic3r::rename_file(outfile, outfile_final)) {
                                boost::nowide::cerr << "Renaming file " << outfile << " to " << outfile_final << " failed" << std::endl;
                                return 1;
                            }
                            outfile = outfile_final;
                        }
                        // Run the post-processing scripts if defined.
                        run_post_process_scripts(outfile, fff_print.full_print_config());
                        boost::nowide::cout << "Slicing result exported to " << outfile << std::endl;
                    } catch (const std::exception &ex) {
                        boost::nowide::cerr << ex.what() << std::endl;
                        return 1;
                    }
/*
                print.center = ! m_config.has("center")
                    && ! m_config.has("align_xy")
                    && ! m_config.opt_bool("dont_arrange");
                print.set_model(model);

                // start chronometer
                typedef std::chrono::high_resolution_clock clock_;
                typedef std::chrono::duration<double, std::ratio<1> > second_;
                std::chrono::time_point<clock_> t0{ clock_::now() };

                const std::string outfile = this->output_filepath(model, IO::Gcode);
                try {
                    print.export_gcode(outfile);
                } catch (std::runtime_error &e) {
                    boost::nowide::cerr << e.what() << std::endl;
                    return 1;
                }
                boost::nowide::cout << "G-code exported to " << outfile << std::endl;

                // output some statistics
                double duration { std::chrono::duration_cast<second_>(clock_::now() - t0).count() };
                boost::nowide::cout << std::fixed << std::setprecision(0)
                    << "Done. Process took " << (duration/60) << " minutes and "
                    << std::setprecision(3)
                    << std::fmod(duration, 60.0) << " seconds." << std::endl
                    << std::setprecision(2)
                    << "Filament required: " << print.total_used_filament() << "mm"
                    << " (" << print.total_extruded_volume()/1000 << "cm3)" << std::endl;
*/
            }
        } else {
            boost::nowide::cerr << "error: option not supported yet: " << opt_key << std::endl;
            return 1;
        }
    }


    if (start_gui) {
#ifdef SLIC3R_GUI
    #if !defined(_WIN32) && !defined(__APPLE__)
        // likely some linux / unix system
        const char *display = boost::nowide::getenv("DISPLAY");
        // const char *wayland_display = boost::nowide::getenv("WAYLAND_DISPLAY");
        //if (! ((display && *display) || (wayland_display && *wayland_display))) {
        if (! (display && *display)) {
            // DISPLAY not set.
            boost::nowide::cerr << "DISPLAY not set, GUI mode not available." << std::endl << std::endl;
            this->print_help(false);
            // Indicate an error.
            return 1;
        }
    #endif // some linux / unix system
        Slic3r::GUI::GUI_InitParams params;
        params.argc = argc;
        params.argv = argv;
        params.load_configs = load_configs;
        params.extra_config = std::move(m_extra_config);
        params.input_files  = std::move(m_input_files);
        params.start_as_gcodeviewer = start_as_gcodeviewer;
        params.start_downloader = start_downloader;
        params.download_url = download_url;
        params.delete_after_load = delete_after_load;
#if ENABLE_GL_CORE_PROFILE
        params.opengl_version = opengl_version;
        params.opengl_debug = opengl_debug;
        params.opengl_compatibiity_profile = opengl_compatibility_profile;
#endif // ENABLE_GL_CORE_PROFILE
        return Slic3r::GUI::GUI_Run(params);
#else // SLIC3R_GUI
        // No GUI support. Just print out a help.
        this->print_help(false);
        // If started without a parameter, consider it to be OK, otherwise report an error code (no action etc).
        return (argc == 0) ? 0 : 1;
#endif // SLIC3R_GUI
    }

    return 0;
}

bool CLI::setup(int argc, char **argv)
{
    {
	    Slic3r::set_logging_level(1);
        const char *loglevel = boost::nowide::getenv("SLIC3R_LOGLEVEL");
        if (loglevel != nullptr) {
            if (loglevel[0] >= '0' && loglevel[0] <= '9' && loglevel[1] == 0)
                set_logging_level(loglevel[0] - '0');
            else
                boost::nowide::cerr << "Invalid SLIC3R_LOGLEVEL environment variable: " << loglevel << std::endl;
        }
    }

    // Detect the operating system flavor after SLIC3R_LOGLEVEL is set.
    detect_platform();

#ifdef WIN32
    // Notify user that a blacklisted DLL was injected into PrusaSlicer process (for example Nahimic, see GH #5573).
    // We hope that if a DLL is being injected into a PrusaSlicer process, it happens at the very start of the application,
    // thus we shall detect them now.
    if (BlacklistedLibraryCheck::get_instance().perform_check()) {
        std::wstring text = L"Following DLLs have been injected into the PrusaSlicer process:\n\n";
        text += BlacklistedLibraryCheck::get_instance().get_blacklisted_string();
        text += L"\n\n"
                L"PrusaSlicer is known to not run correctly with these DLLs injected. "
                L"We suggest stopping or uninstalling these services if you experience "
                L"crashes or unexpected behaviour while using PrusaSlicer.\n"
                L"For example, ASUS Sonic Studio injects a Nahimic driver, which makes PrusaSlicer "
                L"to crash on a secondary monitor, see PrusaSlicer github issue #5573";
        MessageBoxW(NULL, text.c_str(), L"Warning"/*L"Incopatible library found"*/, MB_OK);
    }
#endif

    // See Invoking prusa-slicer from $PATH environment variable crashes #5542
    // boost::filesystem::path path_to_binary = boost::filesystem::system_complete(argv[0]);
    boost::filesystem::path path_to_binary = boost::dll::program_location();

    // Path from the Slic3r binary to its resources.
#ifdef __APPLE__
    // The application is packed in the .dmg archive as 'Slic3r.app/Contents/MacOS/Slic3r'
    // The resources are packed to 'Slic3r.app/Contents/Resources'
    boost::filesystem::path path_resources = boost::filesystem::canonical(path_to_binary).parent_path() / "../Resources";
#elif defined _WIN32
    // The application is packed in the .zip archive in the root,
    // The resources are packed to 'resources'
    // Path from Slic3r binary to resources:
    boost::filesystem::path path_resources = path_to_binary.parent_path() / "resources";
#elif defined SLIC3R_FHS
    // The application is packaged according to the Linux Filesystem Hierarchy Standard
    // Resources are set to the 'Architecture-independent (shared) data', typically /usr/share or /usr/local/share
    boost::filesystem::path path_resources = SLIC3R_FHS_RESOURCES;
#else
    // The application is packed in the .tar.bz archive (or in AppImage) as 'bin/slic3r',
    // The resources are packed to 'resources'
    // Path from Slic3r binary to resources:
    boost::filesystem::path path_resources = boost::filesystem::canonical(path_to_binary).parent_path() / "../resources";
#endif

    set_resources_dir(path_resources.string());
    set_var_dir((path_resources / "icons").string());
    set_local_dir((path_resources / "localization").string());
    set_sys_shapes_dir((path_resources / "shapes").string());
    set_custom_gcodes_dir((path_resources / "custom_gcodes").string());

    // Parse all command line options into a DynamicConfig.
    // If any option is unsupported, print usage and abort immediately.
    t_config_option_keys opt_order;
    if (! m_config.read_cli(argc, argv, &m_input_files, &opt_order)) {
        // Separate error message reported by the CLI parser from the help.
        boost::nowide::cerr << std::endl;
        this->print_help();
        return false;
    }
    // Parse actions and transform options.
    for (auto const &opt_key : opt_order) {
        if (cli_actions_config_def.has(opt_key))
            m_actions.emplace_back(opt_key);
        else if (cli_transform_config_def.has(opt_key))
            m_transforms.emplace_back(opt_key);
    }

    {
        const ConfigOptionInt *opt_loglevel = m_config.opt<ConfigOptionInt>("loglevel");
        if (opt_loglevel != 0)
            set_logging_level(opt_loglevel->value);
    }

    {
        const ConfigOptionInt *opt_threads = m_config.opt<ConfigOptionInt>("threads");
        if (opt_threads != nullptr)
            thread_count = opt_threads->value;
    }

    //FIXME Validating at this stage most likely does not make sense, as the config is not fully initialized yet.
    std::string validity = m_config.validate();

    // Initialize with defaults.
    for (const t_optiondef_map *options : { &cli_actions_config_def.options, &cli_transform_config_def.options, &cli_misc_config_def.options })
        for (const t_optiondef_map::value_type &optdef : *options)
            m_config.option(optdef.first, true);

    set_data_dir(m_config.opt_string("datadir"));
    
    //FIXME Validating at this stage most likely does not make sense, as the config is not fully initialized yet.
    if (!validity.empty()) {
        boost::nowide::cerr << "error: " << validity << std::endl;
        return false;
    }

    return true;
}

void CLI::print_help(bool include_print_options, PrinterTechnology printer_technology) const
{
    boost::nowide::cout
        << SLIC3R_BUILD_ID << " " << "based on Slic3r"
#ifdef SLIC3R_GUI
        << " (with GUI support)"
#else /* SLIC3R_GUI */
        << " (without GUI support)"
#endif /* SLIC3R_GUI */
        << std::endl
        << "https://github.com/prusa3d/PrusaSlicer" << std::endl << std::endl
        << "Usage: prusa-slicer [ ACTIONS ] [ TRANSFORM ] [ OPTIONS ] [ file.stl ... ]" << std::endl
        << std::endl
        << "Actions:" << std::endl;
    cli_actions_config_def.print_cli_help(boost::nowide::cout, false);

    boost::nowide::cout
        << std::endl
        << "Transform options:" << std::endl;
        cli_transform_config_def.print_cli_help(boost::nowide::cout, false);

    boost::nowide::cout
        << std::endl
        << "Other options:" << std::endl;
        cli_misc_config_def.print_cli_help(boost::nowide::cout, false);

    boost::nowide::cout
        << std::endl
        << "Print options are processed in the following order:" << std::endl
        << "\t1) Config keys from the command line, for example --fill-pattern=stars" << std::endl
        << "\t   (highest priority, overwrites everything below)" << std::endl
        << "\t2) Config files loaded with --load" << std::endl
	    << "\t3) Config values loaded from amf or 3mf files" << std::endl;

    if (include_print_options) {
        boost::nowide::cout << std::endl;
        print_config_def.print_cli_help(boost::nowide::cout, true, [printer_technology](const ConfigOptionDef &def)
            { return printer_technology == ptAny || def.printer_technology == ptAny || printer_technology == def.printer_technology; });
    } else {
        boost::nowide::cout
            << std::endl
            << "Run --help-fff / --help-sla to see the full listing of print options." << std::endl;
    }
}

bool CLI::export_models(IO::ExportFormat format)
{
    for (Model &model : m_models) {
        const std::string path = this->output_filepath(model, format);
        bool success = false;
        switch (format) {
            case IO::AMF: success = Slic3r::store_amf(path.c_str(), &model, nullptr, false); break;
            case IO::OBJ: success = Slic3r::store_obj(path.c_str(), &model);          break;
            case IO::STL: success = Slic3r::store_stl(path.c_str(), &model, true);    break;
            case IO::TMF: success = Slic3r::store_3mf(path.c_str(), &model, nullptr, false); break;
            default: assert(false); break;
        }
        if (success)
            std::cout << "File exported to " << path << std::endl;
        else {
            std::cerr << "File export to " << path << " failed" << std::endl;
            return false;
        }
    }
    return true;
}

std::string CLI::output_filepath(const Model &model, IO::ExportFormat format) const
{
    std::string ext;
    switch (format) {
        case IO::AMF: ext = ".zip.amf"; break;
        case IO::OBJ: ext = ".obj"; break;
        case IO::STL: ext = ".stl"; break;
        case IO::TMF: ext = ".3mf"; break;
        default: assert(false); break;
    };
    auto proposed_path = boost::filesystem::path(model.propose_export_file_name_and_path(ext));
    // use --output when available
    std::string cmdline_param = m_config.opt_string("output");
    if (! cmdline_param.empty()) {
        // if we were supplied a directory, use it and append our automatically generated filename
        boost::filesystem::path cmdline_path(cmdline_param);
        if (boost::filesystem::is_directory(cmdline_path))
            proposed_path = cmdline_path / proposed_path.filename();
        else
            proposed_path = cmdline_path;
    }
    return proposed_path.string();
}

// __has_feature() is used later for Clang, this is for compatibility with other compilers (such as GCC and MSVC)
#ifndef __has_feature
#   define __has_feature(x) 0
#endif

#if __has_feature(address_sanitizer) || defined(__SANITIZE_ADDRESS__)
extern "C" {
    // Based on https://github.com/google/skia/blob/main/tools/LsanSuppressions.cpp
    const char *__lsan_default_suppressions() {
        return "leak:libfontconfig\n"           // FontConfig looks like it leaks, but it doesn't.
               "leak:libfreetype\n"             // Unsure, appeared upgrading Debian 9->10.
               "leak:libGLX_nvidia.so\n"        // For NVidia driver.
               "leak:libnvidia-glcore.so\n"     // For NVidia driver.
               "leak:libnvidia-tls.so\n"        // For NVidia driver.
               "leak:terminator_CreateDevice\n" // For Intel Vulkan drivers.
               "leak:swrast_dri.so\n"           // For Mesa 3D software driver.
               "leak:amdgpu_dri.so\n"           // For AMD driver.
               "leak:libdrm_amdgpu.so\n"        // For AMD driver.
               "leak:libdbus-1.so\n"            // For D-Bus library. Unsure if it is a leak or not.
            ;
    }
}
#endif

#if defined(SLIC3R_UBSAN)
extern "C" {
    // Enable printing stacktrace by default. It can be disabled by running PrusaSlicer with "UBSAN_OPTIONS=print_stacktrace=0".
    const char *__ubsan_default_options() {
        return "print_stacktrace=1";
    }
}
#endif

#if defined(_MSC_VER) || defined(__MINGW32__)
extern "C" {
    __declspec(dllexport) int __stdcall slic3r_main(int argc, wchar_t **argv)
    {
        // Convert wchar_t arguments to UTF8.
        std::vector<std::string> 	argv_narrow;
        std::vector<char*>			argv_ptrs(argc + 1, nullptr);
        for (size_t i = 0; i < argc; ++ i)
            argv_narrow.emplace_back(boost::nowide::narrow(argv[i]));
        for (size_t i = 0; i < argc; ++ i)
            argv_ptrs[i] = argv_narrow[i].data();
        // Call the UTF8 main.
        return CLI().run(argc, argv_ptrs.data());
    }
}
#else /* _MSC_VER */
int main(int argc, char **argv)
{
    return CLI().run(argc, argv);
}
#endif /* _MSC_VER */
